package com.ctyun.xstore.vod.auth;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * S3签名
 * 
 * @author zhouzhengrong
 *
 */
public final class Signature {

	private static final Set<String> SUB_RES_SET = new HashSet<>();

	private static Pattern capitalLettersPattern = Pattern.compile("[A-Z]");

	static {
		SUB_RES_SET.add("acl");
		SUB_RES_SET.add("lifecycle");
		SUB_RES_SET.add("location");
		SUB_RES_SET.add("logging");
		SUB_RES_SET.add("notification");
		SUB_RES_SET.add("partNumber");
		SUB_RES_SET.add("policy");
		SUB_RES_SET.add("requestPayment");
		SUB_RES_SET.add("torrent");
		SUB_RES_SET.add("uploadId");
		SUB_RES_SET.add("uploads");
		SUB_RES_SET.add("versionId");
		SUB_RES_SET.add("versioning");
		SUB_RES_SET.add("versions");
		SUB_RES_SET.add("website");
	}

	/**
	 * 生成S3签名
	 * 
	 * @param request
	 * @param accessKey
	 * @param secretKey
	 * @param date
	 * @return
	 */
	public static String sign(final SignParam signParam) {
		String resource = encodedPath(signParam.getUri());

		if (null != signParam.getParameterNames()) {
			Optional<String> subresource = signParam.getParameterNames().stream().sorted().findFirst();
			if (subresource.isPresent() && SUB_RES_SET.contains(subresource.get())) {
				resource += "?" + subresource.get();
			}
		}

		Map<String, String> metasMap = getMetasMap(signParam.getHeaders());
		return sign(signParam.getHttpMethod(), signParam.getAccesskey(), signParam.getSecretKey(),
				signParam.getZoneDateTime(), resource, metasMap);
	}

	private static String encodedPath(String uri) {
		int pathEnd = delimiterOffset(uri, 0, uri.length(), "?#");
		return uri.substring(0, pathEnd);
	}

	private static int delimiterOffset(String input, int pos, int limit, String delimiters) {
		for (int i = pos; i < limit; i++) {
			if (delimiters.indexOf(input.charAt(i)) != -1)
				return i;
		}
		return limit;
	}

	private static Map<String, String> getMetasMap(Map<String, List<String>> headers) {
		Map<String, String> metasMap = new TreeMap<>();
		if (null == headers) {
			return metasMap;
		}
		headers.entrySet().stream().filter(entry -> isAmazonHeaderParameter(entry.getKey()))
				.forEach(entry -> metasMap.put(entry.getKey(), generateMetaStr(entry.getValue())));
		return metasMap;
	}

	private static String generateMetaStr(final List<String> values) {
		return values.stream().sorted().collect(StringBuilder::new,
				(builder, item) -> builder.append(builder.length() == 0 ? "" : ",").append(item), StringBuilder::append)
				.toString();
	}

	/**
	 * 校验是否带有amaz开头的参数
	 * 
	 * @param paramName
	 * @return
	 */
	private static boolean isAmazonHeaderParameter(String paramName) {
		return paramName.startsWith("x-amz-") && !capitalLettersPattern.matcher(paramName).find();
	}

	private static String sign(String httpVerb, String accessKey, String secretKey, String date, String resource,
			Map<String, String> metas) {
		String contentMD5 = "";
		String contentType = "";
		StringBuilder stringToSign = new StringBuilder(
				httpVerb + "\n" + contentMD5.trim() + "\n" + contentType.trim() + "\n" + date + "\n");
		if (metas != null) {
			for (Map.Entry<String, String> entity : metas.entrySet()) {
				stringToSign.append(entity.getKey().trim()).append(":").append(entity.getValue().trim()).append("\n");
			}
		}
		stringToSign.append(resource);
		try {
			Mac mac = Mac.getInstance("HmacSHA1");
			byte[] keyBytes = secretKey.getBytes("UTF8");
			SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
			mac.init(signingKey);
			byte[] signBytes = mac.doFinal(stringToSign.toString().getBytes("UTF8"));
			String signature = encodeBase64(signBytes);
			return "AWS" + " " + accessKey + ":" + signature;
		} catch (Exception e) {
			throw new RuntimeException("MAC CALC FAILED.");
		}
	}

	private static String encodeBase64(byte[] data) {
		String base64 = Base64.getEncoder().encodeToString(data);
		if (base64.endsWith("\r\n"))
			base64 = base64.substring(0, base64.length() - 2);
		if (base64.endsWith("\n"))
			base64 = base64.substring(0, base64.length() - 1);

		return base64;
	}

	public static String getZoneDateTime() {
		return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneId.of("GMT")));
	}

}
